/*
 * Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.util.zip;

import java.io.OutputStream;
import java.io.IOException;

/**
 * This class implements a stream filter for writing compressed data in
 * the GZIP file format.
 *
 * @author David Connelly
 */
public class GZIPOutputStream extends DeflaterOutputStream {

  /**
   * CRC-32 of uncompressed data.
   */
  protected CRC32 crc = new CRC32();

  /*
   * GZIP header magic number.
   */
  private final static int GZIP_MAGIC = 0x8b1f;

  /*
   * Trailer size in bytes.
   *
   */
  private final static int TRAILER_SIZE = 8;

  /**
   * Creates a new output stream with the specified buffer size.
   *
   * <p>The new output stream instance is created as if by invoking
   * the 3-argument constructor GZIPOutputStream(out, size, false).
   *
   * @param out the output stream
   * @param size the output buffer size
   * @throws IOException If an I/O error has occurred.
   * @throws IllegalArgumentException if {@code size <= 0}
   */
  public GZIPOutputStream(OutputStream out, int size) throws IOException {
    this(out, size, false);
  }

  /**
   * Creates a new output stream with the specified buffer size and
   * flush mode.
   *
   * @param out the output stream
   * @param size the output buffer size
   * @param syncFlush if {@code true} invocation of the inherited {@link
   * DeflaterOutputStream#flush() flush()} method of this instance flushes the compressor with flush
   * mode {@link Deflater#SYNC_FLUSH} before flushing the output stream, otherwise only flushes the
   * output stream
   * @throws IOException If an I/O error has occurred.
   * @throws IllegalArgumentException if {@code size <= 0}
   * @since 1.7
   */
  public GZIPOutputStream(OutputStream out, int size, boolean syncFlush)
      throws IOException {
    super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true),
        size,
        syncFlush);
    usesDefaultDeflater = true;
    writeHeader();
    crc.reset();
  }


  /**
   * Creates a new output stream with a default buffer size.
   *
   * <p>The new output stream instance is created as if by invoking
   * the 2-argument constructor GZIPOutputStream(out, false).
   *
   * @param out the output stream
   * @throws IOException If an I/O error has occurred.
   */
  public GZIPOutputStream(OutputStream out) throws IOException {
    this(out, 512, false);
  }

  /**
   * Creates a new output stream with a default buffer size and
   * the specified flush mode.
   *
   * @param out the output stream
   * @param syncFlush if {@code true} invocation of the inherited {@link
   * DeflaterOutputStream#flush() flush()} method of this instance flushes the compressor with flush
   * mode {@link Deflater#SYNC_FLUSH} before flushing the output stream, otherwise only flushes the
   * output stream
   * @throws IOException If an I/O error has occurred.
   * @since 1.7
   */
  public GZIPOutputStream(OutputStream out, boolean syncFlush)
      throws IOException {
    this(out, 512, syncFlush);
  }

  /**
   * Writes array of bytes to the compressed output stream. This method
   * will block until all the bytes are written.
   *
   * @param buf the data to be written
   * @param off the start offset of the data
   * @param len the length of the data
   * @throws IOException If an I/O error has occurred.
   */
  public synchronized void write(byte[] buf, int off, int len)
      throws IOException {
    super.write(buf, off, len);
    crc.update(buf, off, len);
  }

  /**
   * Finishes writing compressed data to the output stream without closing
   * the underlying stream. Use this method when applying multiple filters
   * in succession to the same output stream.
   *
   * @throws IOException if an I/O error has occurred
   */
  public void finish() throws IOException {
    if (!def.finished()) {
      def.finish();
      while (!def.finished()) {
        int len = def.deflate(buf, 0, buf.length);
        if (def.finished() && len <= buf.length - TRAILER_SIZE) {
          // last deflater buffer. Fit trailer at the end
          writeTrailer(buf, len);
          len = len + TRAILER_SIZE;
          out.write(buf, 0, len);
          return;
        }
        if (len > 0) {
          out.write(buf, 0, len);
        }
      }
      // if we can't fit the trailer at the end of the last
      // deflater buffer, we write it separately
      byte[] trailer = new byte[TRAILER_SIZE];
      writeTrailer(trailer, 0);
      out.write(trailer);
    }
  }

  /*
   * Writes GZIP member header.
   */
  private void writeHeader() throws IOException {
    out.write(new byte[]{
        (byte) GZIP_MAGIC,        // Magic number (short)
        (byte) (GZIP_MAGIC >> 8),  // Magic number (short)
        Deflater.DEFLATED,        // Compression method (CM)
        0,                        // Flags (FLG)
        0,                        // Modification time MTIME (int)
        0,                        // Modification time MTIME (int)
        0,                        // Modification time MTIME (int)
        0,                        // Modification time MTIME (int)
        0,                        // Extra flags (XFLG)
        0                         // Operating system (OS)
    });
  }

  /*
   * Writes GZIP member trailer to a byte array, starting at a given
   * offset.
   */
  private void writeTrailer(byte[] buf, int offset) throws IOException {
    writeInt((int) crc.getValue(), buf, offset); // CRC-32 of uncompr. data
    writeInt(def.getTotalIn(), buf, offset + 4); // Number of uncompr. bytes
  }

  /*
   * Writes integer in Intel byte order to a byte array, starting at a
   * given offset.
   */
  private void writeInt(int i, byte[] buf, int offset) throws IOException {
    writeShort(i & 0xffff, buf, offset);
    writeShort((i >> 16) & 0xffff, buf, offset + 2);
  }

  /*
   * Writes short integer in Intel byte order to a byte array, starting
   * at a given offset
   */
  private void writeShort(int s, byte[] buf, int offset) throws IOException {
    buf[offset] = (byte) (s & 0xff);
    buf[offset + 1] = (byte) ((s >> 8) & 0xff);
  }
}
